#
# Copyright 2011 Ugonna Nwakama
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Fetches data for a user with a particular key name
"""
# Python modules
import cgi, logging, datetime, urllib2

# AppEngine modules
from google.appengine.api import images, memcache, capabilities
from google.appengine.ext import db, webapp, deferred # TODO
from google.appengine.ext import deferred
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.runtime.apiproxy_errors import CapabilityDisabledError

# Custom modules
import entities, dsutil, tasks
from util import render_json_response
from util import get_img_url
from util.constants import DATA_TYPE_IMAGE as IMAGE
from util.constants import DATA_TYPE_URL as URL
from util.constants import DATA_TYPE_PHONE_NO as PHONE_NO
from util.constants import DATA_TYPE_PLAIN_TXT as PLAIN_TXT
from util.constants import MODEL_FIELD_DATA_CONTENT as DATA_CONTENT
from util.constants import RESP_HEADER_ERROR_MSG as ERROR_MSG
from util.constants import DATA_PROP_KEY_LIST
from util.errors import InvalidUrlParamException
from util.errors import UnknownClientException

class FetchDataRequestHandler(webapp.RequestHandler):
	"""webapp.RequestHandler class to handle /fetch requests
from clients. Clients are required to pass their stored key name.
	"""

	def get(self):
		key_name = cgi.escape(self.request.get('key_name'), quote=True)
		#TODO Fix
		more_data_avail = True
		try:
			datum_type, datum_sender, datum_to_serve = \
				_get_from_memcache(key_name)
		except Exception:
			try:
				datum_type, datum_sender, datum_to_serve, more_data_avail = \
					get_from_datastore(key_name)
			except db.BadArgumentError:
				self.error(401) # Unauthorized
				logging.error('Client attempted to fetch data with invalid \
					key name %s. \nUser-Agent: %s' % key_name, \
					self.request.headers['User-Agent'])
				return
			except IndexError:
				#TODO Convey proper error message indicating absence of data
				self.error(204) # No Content
				logging.error('Device with key name %s tried to access \
					non-existing data' % key_name)
				
				return
		
		# Format the response data so it can be sent in a JSON serializable way
		response_data = [{'more_data_avail': more_data_avail}, \
			{ 'data': \
				{ 'data_type': datum_type, \
				 'data_sender': datum_sender, \
				 'data_content': datum_to_serve } } ]

		render_json_response(self.response, response_data)
		
class PostDataRequestHandler(webapp.RequestHandler):
	def post(self):
		r_sender_key_name = \
			cgi.escape(urllib2.unquote(self.request.get('sender_key_name')))
		r_owner_key_name = \
			cgi.escape(urllib2.unquote(self.request.get('owner_key_name')))
		r_data_type = cgi.escape(self.request.get('data_type'))
		r_data = self.request.get('data') # Leave data untouched for now

		# If any of the above are empty:
		if not r_sender_key_name or not r_owner_key_name or not r_data_type \
			or not r_data:
			self.error(400) # Bad Request
			logging.error('Bad attempt to post data. Empty attributes')
			return

		try:
			data_key = dsutil.save_data(r_sender_key_name, r_owner_key_name, \
				r_data_type, r_data)
		except (InvalidUrlParamException, UnknownClientException,\
			db.BadValueError), err:
			# self.error(400) # Bad Request
			"""
			Check the specific error and return an error code that the
			client should recognize. If the client key is wrong, return
			err_code as 1. If any other thing is wrong, return err_code as 2.
			db.BadValueError is thrown"""
			err_code = err.__class__ == UnknownClientException and 1 or 2
			response_data = { 'success': False, \
				'message': err.message, \
				'error_code': err_code }
			render_json_response(self.response, response_data)
			logging.error('Bad attempt to post data. Incorrect URL parameters')
			return
		except (CapabilityDisabledError, db.InternalError):
			self.error(503) # Service Unavailable
			logging.error('Server unavailable. Could not save data')
			return
		except:
			self.error(500) # Internal Server Error
			return
		else:
			_set_to_memcache(data_key, r_sender_key_name, r_owner_key_name, \
				r_data_type, r_data)
			logging.info('Data of type %s successfully saved' % r_data_type)
			
			# Add the background task to notify the intended device of new data.
			deferred.defer(tasks.notify_user_of_new_data, r_owner_key_name, \
				_name='notify-client-%s' % data_key)
			# Add the background task to delete this data after 5 minutes, 
			# whether collected by intended client or not.
			deferred.defer(tasks.delete, data_key, _countdown=300, \
				_name='cleanup-data-%s' % data_key)
			# TODO Handle exceptions thrown from defer failure and txn

			response_data = { 'success': True }
			render_json_response(self.response, response_data)
		
class ImageRequestHandler(webapp.RequestHandler):
	"""webapp.RequestHandler that handles serving images to clients.
	"""
	def get(self):
		image_key = cgi.escape(self.request.get('img_key'), quote=True)
		data = entities.ExchangedData.get(image_key)
		
		# Check if data exists and if data type is image
		if data and data.data_type==IMAGE and\
			 hasattr(data, DATA_CONTENT) and \
			 isinstance(getattr(data, DATA_CONTENT), db.Blob):
			 
			# Serves the image as png
			self.response.headers['Content-type'] = 'image/png'
			self.response.out.write(getattr(data, DATA_CONTENT))
		else:
			self.error(404) # Not Found
			return	

def _get_from_memcache(key_name):
	"""Get data that has its namespace as the given key_name from
the memcache.
If the data does not exist in memcache, raise Exception"""

	# If memcache service is unavailable, skip memcache
	if not capabilities.CapabilitySet('memcache').is_enabled():
		return
		
	data = memcache.get_multi(DATA_PROP_KEY_LIST, key_prefix=key_name)
	
	if not data:
		raise Exception('No data in memcache')
		
	datum_type = data[DATA_PROP_KEY_LIST[0]]
	datum_sender = data[DATA_PROP_KEY_LIST[1]]
	datum_to_serve = data[DATA_PROP_KEY_LIST[2]]
	
	# Delete the obtained values from memcache so it cannot be retrieved
	# a second time.
	memcache.delete_multi(DATA_PROP_KEY_LIST, key_prefix=key_name)

	return (datum_type, datum_sender, datum_to_serve)

def _set_to_memcache(key_name, r_sender_key_name, r_owner_key_name, \
	r_data_type, r_data):
	"""Sets data to memcache with a key_prefix of r_owner_key_name
Replaces any data previously existing in that namespace.
Data saved here expires after 120 seconds."""

	# If memcache service is unavailable, skip memcache
	if not capabilities.CapabilitySet('memcache').is_enabled():
		return

	# If the data to save is an image, save its serving URL instead.
	# Else, save as-is.
	memcache_data = r_data_type == IMAGE and get_img_url(key_name) or r_data
	
	memcache_mapping = { DATA_PROP_KEY_LIST[0] : r_data_type, \
		DATA_PROP_KEY_LIST[1] : r_sender_key_name, \
		DATA_PROP_KEY_LIST[2] : memcache_data }
	
	# Set to memcache. Replace any old value that was with the existing key_prefix.
	# Data saved here has a life expectancy of 2 mins.
	failed_keys_list = memcache.set_multi(memcache_mapping, time=120, \
		key_prefix=r_owner_key_name)

	
def get_from_datastore(key_name):
	"""Get data with specified key_name from datastore.
Returns a tuple of the data properties"""
	
	# Old ###
	# query = entities.Device.all()
	# query.filter('__key__ ==', Key.from_path(entities.Device.__name__, key_name))
	# device = query.get()
	
	# New
	device = entities.Device.get_by_key_name(key_name)

	# Check if the device exists
	if not device:
		raise db.BadArgumentError('Device does not exist')
	# Check if device has data	
	try:
		device.all_owned_data
	except IndexError:
		raise 

	# Check if there is more than one data to be collected for this device.
	# If there is more, the client is required to send another request to
	# get the other data.
	if device.all_owned_data.count() > 1:
		more_data_avail = True
	else:
		more_data_avail = False

	# Fetch the first owned data item for this device for sending
	data = device.all_owned_data[0]
		
	# If stored data is an image, obtain the serving URL. Else, serve the
	# data as-is (text-based). Uses the and-or trick.
	if hasattr(data, DATA_CONTENT):
		datum_to_serve = \
			(data.data_type == IMAGE) and \
				get_img_url(data.key()) or \
					getattr(data, DATA_CONTENT)
	else:
		datum_to_serve = None
		logging.debug('Serving empty data to device with keyname %s' % key_name)

	datum_type = data.data_type
	# data.data_sender is the Device object
	datum_sender = data.data_sender.key().name()
	
	return (datum_type, datum_sender, datum_to_serve, more_data_avail)

_application = webapp.WSGIApplication([('/fetch/image', ImageRequestHandler),
	('/fetch', FetchDataRequestHandler), \
	('/post', PostDataRequestHandler)], debug=True)
	
def main():
	logging.getLogger().setLevel(logging.DEBUG)
	run_wsgi_app(_application)
	
if __name__ == '__main__':
	main()
